const Signals = imports.signals;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
-const Pango = imports.gi.Pango;
const Gtk = imports.gi.Gtk;
-
-const Tidy = imports.gi.Tidy;
-const Big = imports.gi.Big;
-const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
+const GenericDisplay = imports.ui.genericDisplay;
+
// TODO - move this into GConf once we're not a plugin anymore
// but have taken over metacity
// This list is taken from GNOME Online popular applications
'vncviewer.desktop'
];
-const APPDISPLAY_NAME_COLOR = new Clutter.Color();
-APPDISPLAY_NAME_COLOR.from_pixel(0xffffffff);
-const APPDISPLAY_COMMENT_COLOR = new Clutter.Color();
-APPDISPLAY_COMMENT_COLOR.from_pixel(0xffffffbb);
-const APPDISPLAY_BACKGROUND_COLOR = new Clutter.Color();
-APPDISPLAY_BACKGROUND_COLOR.from_pixel(0x000000ff);
-const APPDISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color();
-APPDISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055);
-
-const APPDISPLAY_HEIGHT = 50;
-const APPDISPLAY_PADDING = 4;
-
-function AppDisplayItem(node, width) {
- this._init(node, width);
+/* This class represents a single display item containing information about an application.
+ *
+ * appInfo - GAppInfo object containing information about the application
+ * availableWidth - total width available for the item
+ */
+function AppDisplayItem(appInfo, availableWidth) {
+ this._init(appInfo, availableWidth);
}
AppDisplayItem.prototype = {
- _init: function(appInfo, width) {
- let me = this;
+ __proto__: GenericDisplay.GenericDisplayItem.prototype,
+
+ _init : function(appInfo, availableWidth) {
+ GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
this._appInfo = appInfo;
let name = appInfo.get_name();
- let iconTheme = Gtk.IconTheme.get_default();
+ let description = appInfo.get_description();
- this._group = new Clutter.Group({reactive: true,
- width: width,
- height: APPDISPLAY_HEIGHT});
- this._group.connect('button-press-event', function(group, e) {
- me.emit('activate');
- return true;
- });
- this._bg = new Big.Box({ background_color: APPDISPLAY_BACKGROUND_COLOR,
- corner_radius: 4,
- x: 0, y: 0,
- width: width, height: APPDISPLAY_HEIGHT });
- this._group.add_actor(this._bg);
+ let iconTheme = Gtk.IconTheme.get_default();
- this._icon = new Clutter.Texture({ width: 48, height: 48, x: 0, y: 0 });
+ let icon = new Clutter.Texture({ width: 48, height: 48});
let gicon = appInfo.get_icon();
let path = null;
if (gicon != null) {
path = iconinfo.get_filename();
}
- if (path)
- this._icon.set_from_file(path);
- this._group.add_actor(this._icon);
-
- let comment = appInfo.get_description();
- let text_width = width - (me._icon.width + 4);
- this._name = new Clutter.Label({ color: APPDISPLAY_NAME_COLOR,
- font_name: "Sans 14px",
- width: text_width,
- ellipsize: Pango.EllipsizeMode.END,
- text: name,
- x: this._icon.width + 4,
- y: 0});
- this._group.add_actor(this._name);
- this._comment = new Clutter.Label({ color: APPDISPLAY_COMMENT_COLOR,
- font_name: "Sans 12px",
- width: text_width,
- ellipsize: Pango.EllipsizeMode.END,
- text: comment,
- x: this._name.x,
- y: this._name.height + 4})
- this._group.add_actor(this._comment);
- this.actor = this._group;
- },
- launch: function() {
- this._appInfo.launch([], null);
+ if (path) {
+ try {
+ icon.set_from_file(path);
+ } catch (e) {
+ // we can get an error here if the file path doesn't exist on the system
+ log('Error loading AppDisplayItem icon ' + e);
+ }
+ }
+ this._setItemInfo(name, description, icon);
},
- appInfo: function () {
+
+ //// Public methods ////
+
+ // Returns the application info associated with this display item.
+ getAppInfo : function () {
return this._appInfo;
},
- markSelected: function(isSelected) {
- let color;
- if (isSelected)
- color = APPDISPLAY_SELECTED_BACKGROUND_COLOR;
- else
- color = APPDISPLAY_BACKGROUND_COLOR;
- this._bg.background_color = color;
+
+ //// Public method overrides ////
+
+ // Opens an application represented by this display item.
+ launch : function() {
+ this._appInfo.launch([], null);
}
-};
-Signals.addSignalMethods(AppDisplayItem.prototype);
+};
+/* This class represents a display containing a collection of application items.
+ * The applications are sorted based on their popularity by default, and based on
+ * their name if some search filter is applied.
+ *
+ * width - width available for the display
+ * height - height available for the display
+ */
function AppDisplay(width, height) {
this._init(width, height);
}
AppDisplay.prototype = {
+ __proto__: GenericDisplay.GenericDisplay.prototype,
+
_init : function(width, height) {
+ GenericDisplay.GenericDisplay.prototype._init.call(this, width, height);
let me = this;
- let global = Shell.Global.get();
- this._search = '';
- this._width = width;
- this._height = height;
this._appMonitor = new Shell.AppMonitor();
this._appsStale = true;
this._appMonitor.connect('changed', function(mon) {
me._appsStale = true;
+ // We still need to determine what events other than search can trigger
+ // a change in the set of applications that are being shown while the
+ // user in in the overlay mode, however let's redisplay just in case.
+ me._redisplay();
});
- this._grid = new Tidy.Grid({width: width, height: height});
- this._appSet = {}; // Map<appId, appInfo>
- this._displayed = {}; // Map<appId, AppDisplay>
- this._selectedIndex = -1;
- this._maxItems = this._height / (APPDISPLAY_HEIGHT + APPDISPLAY_PADDING);
- this.actor = this._grid;
},
- _refreshCache: function() {
- let me = this;
+ //// Protected method overrides ////
+ // Gets information about all applications by calling Gio.app_info_get_all().
+ _refreshCache : function() {
+ let me = this;
if (!this._appsStale)
return;
- for (id in this._displayed)
- this._displayed[id].destroy();
- this._appSet = {};
- this._displayed = {};
- this._selectedIndex = -1;
+ this._allItems = {};
let apps = Gio.app_info_get_all();
for (let i = 0; i < apps.length; i++) {
let appInfo = apps[i];
let appId = appInfo.get_id();
- this._appSet[appId] = appInfo;
+ this._allItems[appId] = appInfo;
}
this._appsStale = false;
},
- _removeItem: function(appId) {
- let item = this._displayed[appId];
- let group = item.actor;
- group.destroy();
- delete this._displayed[appId];
- },
-
- _removeAll: function() {
- for (appId in this._displayed)
- this._removeItem(appId);
- },
-
- _setDefaultList: function() {
- this._removeAll();
+ // Sets the list of the displayed items based on the list of DEFAULT_APPLICATIONS.
+ _setDefaultList : function() {
+ this._removeAllDisplayItems();
let added = 0;
for (let i = 0; i < DEFAULT_APPLICATIONS.length && added < this._maxItems; i++) {
let appId = DEFAULT_APPLICATIONS[i];
- let appInfo = this._appSet[appId];
+ let appInfo = this._allItems[appId];
if (appInfo) {
- this._filterAdd(appId);
- added += 1;
+ this._addDisplayItem(appId);
+ added += 1;
}
}
},
- _getNDisplayed: function() {
- // Is there a better way to do .size() ?
- let c = 0; for (i in this._displayed) { c += 1; };
- return c;
- },
-
- _filterAdd: function(appId) {
+ // Sorts the list of item ids in-place based on the alphabetical order of the names of
+ // the items associated with the ids.
+ _sortItems : function(itemIds) {
let me = this;
-
- let appInfo = this._appSet[appId];
-
- let appDisplayItem = new AppDisplayItem(appInfo, this._width);
- appDisplayItem.connect('activate', function() {
- appDisplayItem.launch();
- me.emit('activated');
+ itemIds.sort(function (a,b) {
+ let appA = me._allItems[a];
+ let appB = me._allItems[b];
+ return appA.get_name().localeCompare(appB.get_name());
});
- let group = appDisplayItem.actor;
- this._grid.add_actor(group);
- this._displayed[appId] = appDisplayItem;
- },
-
- _filterRemove: function(appId) {
- // In the future, do some sort of fade out or other effect here
- let item = this._displayed[appId];
- this._removeItem(item);
},
- _appInfoMatches: function(appInfo, search) {
+ // Checks if the item info can be a match for the search string by checking
+ // the name, description, and execution command for the application.
+ // Item info is expected to be GAppInfo.
+ // Returns a boolean flag indicating if itemInfo is a match.
+ _isInfoMatching : function(itemInfo, search) {
if (search == null || search == '')
return true;
- let name = appInfo.get_name().toLowerCase();
+ let name = itemInfo.get_name().toLowerCase();
if (name.indexOf(search) >= 0)
return true;
- let description = appInfo.get_description();
+ let description = itemInfo.get_description();
if (description) {
description = description.toLowerCase();
if (description.indexOf(search) >= 0)
return true;
}
- let exec = appInfo.get_executable().toLowerCase();
+ let exec = itemInfo.get_executable().toLowerCase();
if (exec.indexOf(search) >= 0)
return true;
return false;
},
- _sortApps: function(appIds) {
- let me = this;
- return appIds.sort(function (a,b) {
- let appA = me._appSet[a];
- let appB = me._appSet[b];
- return appA.get_name().localeCompare(appB.get_name());
- });
- },
-
- _doSearchFilter: function() {
- this._removeAll();
- let matchedApps = [];
- for (appId in this._appSet) {
- if (matchedApps.length >= this._maxItems)
- break;
- if (this._displayed[appId])
- continue;
- let app = this._appSet[appId];
- if (this._appInfoMatches(app, this._search))
- matchedApps.push(appId);
- }
- this._sortApps(matchedApps);
- for (let i = 0; i < matchedApps.length; i++) {
- this._filterAdd(matchedApps[i]);
- }
- },
-
- _redisplay: function() {
- this._refreshCache();
- if (!this._search)
- this._setDefaultList();
- else
- this._doSearchFilter();
- },
-
- setSearch: function(text) {
- this._search = text.toLowerCase();
- this._redisplay();
- },
-
- _findDisplayedByIndex: function(index) {
- let displayedActors = this._grid.get_children();
- let actor = displayedActors[index];
- return this._findDisplayedByActor(actor);
- },
-
- _findDisplayedByActor: function(actor) {
- for (appId in this._displayed) {
- let item = this._displayed[appId];
- if (item.actor == actor) {
- return item;
- }
- }
- return null;
- },
-
- searchActivate: function() {
- if (this._selectedIndex != -1) {
- let selected = this._findDisplayedByIndex(this._selectedIndex);
- selected.launch();
- this.emit('activated');
- return;
- }
- let displayedActors = this._grid.get_children();
- if (displayedActors.length != 1)
- return;
- let selectedActor = displayedActors[0];
- let selectedMenuItem = this._findDisplayedByActor(selectedActor);
- selectedMenuItem.launch();
- this.emit('activated');
- },
-
- _selectIndex: function(index) {
- if (this._selectedIndex != -1) {
- let prev = this._findDisplayedByIndex(this._selectedIndex);
- prev.markSelected(false);
- }
- this._selectedIndex = index;
- let item = this._findDisplayedByIndex(index);
- item.markSelected(true);
- },
-
- selectUp: function() {
- let prev = this._selectedIndex-1;
- if (prev < 0)
- return;
- this._selectIndex(prev);
- },
-
- selectDown: function() {
- let next = this._selectedIndex+1;
- let nDisplayed = this._getNDisplayed();
- if (next >= nDisplayed)
- return;
- this._selectIndex(next);
- },
-
- show: function() {
- this._redisplay();
- this._grid.show();
- },
-
- hide: function() {
- this._grid.hide();
- }
+ // Creates an AppDisplayItem based on itemInfo, which is expected be a GAppInfo object.
+ _createDisplayItem: function(itemInfo) {
+ return new AppDisplayItem(itemInfo, this._width);
+ }
};
Signals.addSignalMethods(AppDisplay.prototype);
--- /dev/null
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Signals = imports.signals;
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+
+const Shell = imports.gi.Shell;
+
+const GenericDisplay = imports.ui.genericDisplay;
+
+/* This class represents a single display item containing information about a document.
+ *
+ * docInfo - GtkRecentInfo object containing information about the document
+ * availableWidth - total width available for the item
+ */
+function DocDisplayItem(docInfo, availableWidth) {
+ this._init(docInfo, availableWidth);
+}
+
+DocDisplayItem.prototype = {
+ __proto__: GenericDisplay.GenericDisplayItem.prototype,
+
+ _init : function(docInfo, availableWidth) {
+ GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
+ this._docInfo = docInfo;
+
+ let name = docInfo.get_display_name();
+
+ // we can possibly display tags in the space for description in the future
+ let description = "";
+
+ let icon = new Clutter.Texture({width: 48, height: 48});
+ Shell.clutter_texture_set_from_pixbuf(icon, docInfo.get_icon(48));
+
+ this._setItemInfo(name, description, icon);
+ },
+
+ //// Public methods ////
+
+ // Returns the document info associated with this display item.
+ getDocInfo : function() {
+ return this._docInfo;
+ },
+
+ //// Public method overrides ////
+
+ // Opens a document represented by this display item.
+ launch : function() {
+ let appName = this._docInfo.last_application();
+ let appData = this._docInfo.get_application_info(appName);
+ let success = appData[0];
+ let appExec = appData[1];
+ let count = appData[2];
+ let time = appData[3];
+ if (success) {
+ log("Will open a document with the following command: " + appExec);
+ // TODO: Change this once better support for creating GAppInfo is added to
+ // GtkRecentInfo, as right now this relies on the fact that the file uri is
+ // already a part of appExec, so we don't supply any files to appInfo.launch().
+ let appInfo = Gio.app_info_create_from_commandline(appExec, null, 0, null);
+ appInfo.launch([], null, null);
+ } else {
+ log("Failed to get application info for " + this._docInfo.get_uri());
+ }
+ }
+
+};
+
+/* This class represents a display containing a collection of document items.
+ * The documents are sorted by how recently they were last visited.
+ *
+ * width - width available for the display
+ * height - height available for the display
+ */
+function DocDisplay(width, height) {
+ this._init(width, height);
+}
+
+DocDisplay.prototype = {
+ __proto__: GenericDisplay.GenericDisplay.prototype,
+
+ _init : function(width, height) {
+ GenericDisplay.GenericDisplay.prototype._init.call(this, width, height);
+ let me = this;
+ this._recentManager = Gtk.RecentManager.get_default();
+ this._docsStale = true;
+ this._recentManager.connect('changed', function(recentManager, userData) {
+ me._docsStale = true;
+ // Changes in local recent files should not happen when we are in the overlay mode,
+ // but redisplaying right away is cool when we use Zephyr.
+ // Also, we might be displaying remote documents, like Google Docs, in the future
+ // which might be edited by someone else.
+ me._redisplay();
+ });
+ },
+
+ //// Protected method overrides ////
+
+ // Gets the list of recent items from the recent items manager.
+ _refreshCache : function() {
+ let me = this;
+ if (!this._docsStale)
+ return;
+ this._allItems = {};
+ let docs = this._recentManager.get_items();
+ for (let i = 0; i < docs.length; i++) {
+ let docInfo = docs[i];
+ let docId = docInfo.get_uri();
+ // we use GtkRecentInfo URI as an item Id
+ this._allItems[docId] = docInfo;
+ }
+ this._docsStale = false;
+ },
+
+ // Sets the list of the displayed items based on how recently they were last visited.
+ _setDefaultList : function() {
+ this._removeAllDisplayItems();
+
+ // It seems to be an implementation detail of the Mozilla JavaScript that object
+ // properties are returned during the iteration in the same order in which they were
+ // defined, but it is not a guarantee according to this
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Statements/for...in
+ // So while this._allItems associative array seems to always be ordered by last visited,
+ // as the results of this._recentManager.get_items() based on which it is constructed are,
+ // we should do the sorting manually anyway.
+ // TODO: would it be better to store an additional array of doc ids as they are
+ // returned by this._recentManager.get_items() to avoid having to do this sorting?
+ // This function is called each time the search string is set back to '', so we are
+ // doing the sorting over the same items multiple times.
+ let docIds = [];
+ for (docId in this._allItems) {
+ docIds.push(docId);
+ }
+ this._sortItems(docIds);
+
+ let added = 0;
+ for (let i = 0; i < docIds.length && added < this._maxItems; i++) {
+ let docInfo = this._allItems[docIds[i]];
+ // docInfo.exists() checks if the resource still exists
+ if (docInfo.exists()) {
+ this._addDisplayItem(docIds[i]);
+ added += 1;
+ }
+ }
+ },
+
+ // Sorts the list of item ids in-place based on how recently the items associated with
+ // the ids were last visited.
+ _sortItems : function(itemIds) {
+ let me = this;
+ itemIds.sort(function (a,b) {
+ let docA = me._allItems[a];
+ let docB = me._allItems[b];
+ if (docA.get_visited() > docB.get_visited())
+ return -1;
+ else if (docA.get_visited() < docB.get_visited())
+ return 1;
+ else
+ return 0;
+ });
+ },
+
+ // Checks if the item info can be a match for the search string by checking
+ // the name of the document. Item info is expected to be GtkRecentInfo.
+ // Returns a boolean flag indicating if itemInfo is a match.
+ _isInfoMatching : function(itemInfo, search) {
+ if (search == null || search == '')
+ return true;
+ let name = itemInfo.get_display_name().toLowerCase();
+ if (name.indexOf(search) >= 0)
+ return true;
+ // TODO: we can also check doc URIs, so that
+ // if you search for a directory name, we display recent files from it
+ return false;
+ },
+
+ // Creates a DocDisplayItem based on itemInfo, which is expected be a GtkRecentInfo object.
+ _createDisplayItem: function(itemInfo) {
+ return new DocDisplayItem(itemInfo, this._width);
+ }
+};
+
+Signals.addSignalMethods(DocDisplay.prototype);
--- /dev/null
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Signals = imports.signals;
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Pango = imports.gi.Pango;
+const Gtk = imports.gi.Gtk;
+
+const Tidy = imports.gi.Tidy;
+const Big = imports.gi.Big;
+const Shell = imports.gi.Shell;
+
+const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color();
+ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff);
+const ITEM_DISPLAY_DESCRIPTION_COLOR = new Clutter.Color();
+ITEM_DISPLAY_DESCRIPTION_COLOR.from_pixel(0xffffffbb);
+const ITEM_DISPLAY_BACKGROUND_COLOR = new Clutter.Color();
+ITEM_DISPLAY_BACKGROUND_COLOR.from_pixel(0x00000000);
+const ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color();
+ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055);
+
+const ITEM_DISPLAY_HEIGHT = 50;
+const ITEM_DISPLAY_PADDING = 4;
+
+/* This is a virtual class that represents a single display item containing
+ * a name, a description, and an icon. It allows selecting an item and represents
+ * it by highlighting it with a different background color than the default.
+ *
+ * availableWidth - total width available for the item
+ */
+function GenericDisplayItem(availableWidth) {
+ this._init(name, description, icon, availableWidth);
+}
+
+GenericDisplayItem.prototype = {
+ _init: function(availableWidth) {
+ this._availableWidth = availableWidth;
+ let me = this;
+
+ this._group = new Clutter.Group({reactive: true,
+ width: availableWidth,
+ height: ITEM_DISPLAY_HEIGHT});
+ this._group.connect('button-press-event', function(group, e) {
+ me.emit('activate');
+ return true;
+ });
+
+ this._bg = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
+ corner_radius: 4,
+ x: 0, y: 0,
+ width: availableWidth, height: ITEM_DISPLAY_HEIGHT });
+ this._group.add_actor(this._bg);
+
+ this._name = null;
+ this._description = null;
+ this._icon = null;
+
+ this.actor = this._group;
+ },
+
+ //// Public methods ////
+
+ // Highlights the item by setting a different background color than the default
+ // if isSelected is true, removes the highlighting otherwise.
+ markSelected: function(isSelected) {
+ let color;
+ if (isSelected)
+ color = ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR;
+ else
+ color = ITEM_DISPLAY_BACKGROUND_COLOR;
+ this._bg.background_color = color;
+ },
+
+ //// Pure virtual public methods ////
+
+ // Performes an action associated with launching this item, such as opening a file or an application.
+ launch: function() {
+ throw new Error("Not implemented");
+ },
+
+ //// Protected methods ////
+
+ /*
+ * Creates the graphical elements for the item based on the item information.
+ *
+ * nameText - name of the item
+ * descriptionText - short description of the item
+ * iconActor - ClutterTexture containing the icon image which should be 48x48 pixels
+ */
+ _setItemInfo: function(nameText, descriptionText, iconActor) {
+ if (this._name != null) {
+ // this also removes this._name from the parent container,
+ // so we don't need to call this._group.remove_actor(this._name) directly
+ this._name.destroy();
+ this._name = null;
+ }
+ if (this._description != null) {
+ this._description.destroy();
+ this._description = null;
+ }
+ if (this._icon != null) {
+ // though we get the icon from elsewhere, we assume its ownership here,
+ // and therefore should be responsible for distroying it
+ this._icon.destroy();
+ this._icon = null;
+ }
+
+ this._icon = iconActor;
+ this._icon.x = 0;
+ this._icon.y = 0;
+ this._group.add_actor(this._icon);
+
+ let text_width = this._availableWidth - (this._icon.width + 4);
+ this._name = new Clutter.Label({ color: ITEM_DISPLAY_NAME_COLOR,
+ font_name: "Sans 14px",
+ width: text_width,
+ ellipsize: Pango.EllipsizeMode.END,
+ text: nameText,
+ x: this._icon.width + 4,
+ y: 0});
+ this._group.add_actor(this._name);
+ this._description = new Clutter.Label({ color: ITEM_DISPLAY_DESCRIPTION_COLOR,
+ font_name: "Sans 12px",
+ width: text_width,
+ ellipsize: Pango.EllipsizeMode.END,
+ text: descriptionText,
+ x: this._name.x,
+ y: this._name.height + 4})
+ this._group.add_actor(this._description);
+ }
+};
+
+Signals.addSignalMethods(GenericDisplayItem.prototype);
+
+/* This is a virtual class that represents a display containing a collection of items
+ * that can be filtered with a search string.
+ *
+ * width - width available for the display
+ * height - height available for the display
+ */
+function GenericDisplay(width, height) {
+ this._init(width, height);
+}
+
+GenericDisplay.prototype = {
+ _init : function(width, height) {
+ this._search = '';
+ this._width = width;
+ this._height = height;
+ this._grid = new Tidy.Grid({width: width, height: height});
+ // map<itemId, Object> where Object represents the item info
+ this._allItems = {};
+ // map<itemId, GenericDisplayItem>
+ this._displayedItems = {};
+ this._displayedItemsCount = 0;
+ // GenericDisplayItem
+ this._activatedItem = null;
+ this._selectedIndex = -1;
+ this._keepDisplayCurrent = false;
+ this._maxItems = this._height / (ITEM_DISPLAY_HEIGHT + ITEM_DISPLAY_PADDING);
+ this.actor = this._grid;
+ },
+
+ //// Public methods ////
+
+ // Sets the search string and displays the matching items.
+ setSearch: function(text) {
+ this._search = text.toLowerCase();
+ this._redisplay();
+ },
+
+ // Sets this._activatedItem to the item that is selected and emits 'activated' signal.
+ // The reason we don't call launch() on the activated item right away is because we want
+ // the class that contains the display to do all other necessary actions and then call
+ // doActivate(). Currently, when a selected item is activated we only clear the search
+ // entry, but when an item that was not selected is clicked, we want to move the selection
+ // to the clicked item first. This needs to happen in the class that contains the display
+ // because the selection might be moved from some other display that class contains.
+ activateSelected: function() {
+ if (this._selectedIndex != -1) {
+ let selected = this._findDisplayedByIndex(this._selectedIndex);
+ this._activatedItem = selected;
+ this.emit('activated');
+ }
+ },
+
+ // Moves the selection one up. If the selection was already on the top item, it's moved
+ // to the bottom one. Returns true if the selection actually moved up, false if it wrapped
+ // around to the bottom.
+ selectUp: function() {
+ let selectedUp = true;
+ let prev = this._selectedIndex - 1;
+ if (this._selectedIndex <= 0) {
+ prev = this._displayedItemsCount - 1;
+ selectedUp = false;
+ }
+ this._selectIndex(prev);
+ return selectedUp;
+ },
+
+ // Moves the selection one down. If the selection was already on the bottom item, it's moved
+ // to the top one. Returns true if the selection actually moved down, false if it wrapped
+ // around to the top.
+ selectDown: function() {
+ let selectedDown = true;
+ let next = this._selectedIndex + 1;
+ if (this._selectedIndex == this._displayedItemsCount - 1) {
+ next = 0;
+ selectedDown = false;
+ }
+ this._selectIndex(next);
+ return selectedDown;
+ },
+
+ // Selects the first item among the displayed items.
+ selectFirstItem: function() {
+ if (this.hasItems())
+ this._selectIndex(0);
+ },
+
+ // Selects the last item among the displayed items.
+ selectLastItem: function() {
+ if (this.hasItems())
+ this._selectIndex(this._displayedItemsCount - 1);
+ },
+
+ // Returns true if the display has some item selected.
+ hasSelected: function() {
+ return this._selectedIndex != -1;
+ },
+
+ // Removes selection if some display item is selected.
+ unsetSelected: function() {
+ this._selectIndex(-1);
+ },
+
+ // Returns true if the display has any displayed items.
+ hasItems: function() {
+ return this._displayedItemsCount > 0;
+ },
+
+ // Highlights the activated item and launches it.
+ doActivate: function() {
+ if (this._activatedItem != null) {
+ // We update the selection, so that in case an item was selected by clicking on it and
+ // it is different from an item that was previously selected, we can highlight the new selection.
+ this._selectIndex(this._getIndexOfDisplayedActor(this._activatedItem.actor));
+ this._activatedItem.launch();
+ }
+ },
+
+ // Updates the displayed items and makes the display actor visible.
+ show: function() {
+ this._keepDisplayCurrent = true;
+ this._redisplay();
+ this._grid.show();
+ },
+
+ // Hides the display actor.
+ hide: function() {
+ this._grid.hide();
+ this._keepDisplayCurrent = false;
+ },
+
+ //// Protected methods ////
+
+ // Creates a display item based on the information associated with itemId
+ // and adds it to the displayed items.
+ _addDisplayItem : function(itemId) {
+ if (this._displayedItems.hasOwnProperty(itemId)) {
+ log("Tried adding a display item for " + itemId + ", but an item with this item id is already among displayed items.");
+ return;
+ }
+
+ let me = this;
+
+ let itemInfo = this._allItems[itemId];
+ let displayItem = this._createDisplayItem(itemInfo);
+
+ displayItem.connect('activate', function() {
+ me._activatedItem = displayItem;
+ me.emit('activated');
+ });
+ this._grid.add_actor(displayItem.actor);
+ this._displayedItems[itemId] = displayItem;
+ this._displayedItemsCount++;
+ },
+
+ // Removes an item identifed by the itemId from the displayed items.
+ _removeDisplayItem: function(itemId) {
+ let displayItem = this._displayedItems[itemId];
+ displayItem.actor.destroy();
+ delete this._displayedItems[itemId];
+ this._displayedItemsCount--;
+ },
+
+ // Removes all displayed items.
+ _removeAllDisplayItems: function() {
+ for (itemId in this._displayedItems)
+ this._removeDisplayItem(itemId);
+ },
+
+ // Updates the displayed items, applying the search string if one exists.
+ _redisplay: function() {
+ if (!this._keepDisplayCurrent)
+ return;
+
+ this._refreshCache();
+ if (!this._search)
+ this._setDefaultList();
+ else
+ this._doSearchFilter();
+
+ if (this.hasSelected()) {
+ this._selectedIndex = -1;
+ this.selectFirstItem();
+ }
+
+ this.emit('redisplayed');
+ },
+
+ //// Pure virtual protected methods ////
+
+ // Performs the steps needed to have the latest information about the items.
+ _refreshCache: function() {
+ throw new Error("Not implemented");
+ },
+
+ // Sets the list of the displayed items based on the default sorting order.
+ // The default sorting order is specific to each implementing class.
+ _setDefaultList: function() {
+ throw new Error("Not implemented");
+ },
+
+ // Sorts the list of item ids in-place based on the order in which the items
+ // associated with the ids should be displayed.
+ _sortItems: function(itemIds) {
+ throw new Error("Not implemented");
+ },
+
+ // Checks if the item info can be a match for the search string.
+ // Returns a boolean flag indicating if that's the case.
+ _isInfoMatching: function(itemInfo, search) {
+ throw new Error("Not implemented");
+ },
+
+ // Creates a display item based on itemInfo.
+ _createDisplayItem: function(itemInfo) {
+ throw new Error("Not implemented");
+ },
+
+ //// Private methods ////
+
+ // Applies the search string to the list of items to find matches,
+ // and displays up to this._maxItems that matched.
+ _doSearchFilter: function() {
+ this._removeAllDisplayItems();
+ let matchedItems = [];
+ for (itemId in this._allItems) {
+ let item = this._allItems[itemId];
+ if (this._isInfoMatching(item, this._search))
+ matchedItems.push(itemId);
+ }
+ this._sortItems(matchedItems);
+ for (let i = 0; i < matchedItems.length && i < this._maxItems; i++) {
+ this._addDisplayItem(matchedItems[i]);
+ }
+ },
+
+ // Returns a display item based on its index in the ordering of the
+ // display children.
+ _findDisplayedByIndex: function(index) {
+ let displayedActors = this._grid.get_children();
+ let actor = displayedActors[index];
+ return this._findDisplayedByActor(actor);
+ },
+
+ // Returns a display item based on the actor that represents it in
+ // the display.
+ _findDisplayedByActor: function(actor) {
+ for (itemId in this._displayedItems) {
+ let item = this._displayedItems[itemId];
+ if (item.actor == actor) {
+ return item;
+ }
+ }
+ return null;
+ },
+
+ // Returns and index that the actor has in the ordering of the display's
+ // children.
+ _getIndexOfDisplayedActor: function(actor) {
+ let children = this._grid.get_children();
+ for (let i = 0; i < children.length; i++) {
+ if (children[i] == actor)
+ return i;
+ }
+ return -1;
+ },
+
+ // Selects (e.g. highlights) a display item at the provided index.
+ _selectIndex: function(index) {
+ if (this._selectedIndex != -1) {
+ let prev = this._findDisplayedByIndex(this._selectedIndex);
+ prev.markSelected(false);
+ }
+ this._selectedIndex = index;
+ if (index != -1 && index < this._displayedItemsCount) {
+ let item = this._findDisplayedByIndex(index);
+ item.markSelected(true);
+ }
+ }
+};
+
+Signals.addSignalMethods(GenericDisplay.prototype);
const Workspaces = imports.ui.workspaces;
const Main = imports.ui.main;
const Panel = imports.ui.panel;
-const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const Big = imports.gi.Big;
const AppDisplay = imports.ui.appDisplay;
+const DocDisplay = imports.ui.docDisplay;
const OVERLAY_BACKGROUND_COLOR = new Clutter.Color();
OVERLAY_BACKGROUND_COLOR.from_pixel(0x000000ff);
const SIDESHOW_PAD = 6;
+const SIDESHOW_PAD_BOTTOM = 60;
const SIDESHOW_MIN_WIDTH = 250;
+const SIDESHOW_SECTION_PAD = 10;
+const SIDESHOW_SECTION_LABEL_PAD_BOTTOM = 6;
const SIDESHOW_SEARCH_BG_COLOR = new Clutter.Color();
SIDESHOW_SEARCH_BG_COLOR.from_pixel(0xffffffff);
const SIDESHOW_TEXT_COLOR = new Clutter.Color();
searchIconTexture.set_from_file(searchIconPath);
this.actor.add_actor(searchIconTexture);
+ // We need to initialize the text for the entry to have the cursor displayed
+ // in it. See http://bugzilla.openedhand.com/show_bug.cgi?id=1365
this._searchEntry = new Clutter.Entry({
font_name: "Sans 14px",
x: searchIconTexture.x
+ searchIconTexture.width + 4,
y: searchIconTexture.y,
width: rect.width - (searchIconTexture.x),
- height: searchIconTexture.height});
+ height: searchIconTexture.height,
+ text: ""});
this.actor.add_actor(this._searchEntry);
- global.stage.set_key_focus(this._searchEntry);
+ global.stage.set_key_focus(this._searchEntry);
this._searchQueued = false;
this._searchActive = false;
this._searchEntry.connect('notify::text', function (se, prop) {
me._searchQueued = false;
me._searchActive = text != '';
me._appDisplay.setSearch(text);
+ me._docDisplay.setSearch(text);
return false;
});
});
this._searchEntry.connect('activate', function (se) {
- me._searchEntry.text = '';
- me._appDisplay.searchActivate();
+ // only one of the displays will have an item selected, so it's ok to
+ // call activateSelected() on both of them
+ me._appDisplay.activateSelected();
+ me._docDisplay.activateSelected();
return true;
});
this._searchEntry.connect('key-press-event', function (se, e) {
me._searchEntry.text = '';
return true;
} else if (code == 111) {
- me._appDisplay.selectUp();
+ // selectUp and selectDown wrap around in their respective displays
+ // too, but there doesn't seem to be any flickering if we first select
+ // something in one display, but then unset the selection, and move
+ // it to the other display, so it's ok to do that.
+ if (me._appDisplay.hasSelected() && !me._appDisplay.selectUp() && me._docDisplay.hasItems()) {
+ me._appDisplay.unsetSelected();
+ me._docDisplay.selectLastItem();
+ } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectUp() && me._appDisplay.hasItems()) {
+ me._docDisplay.unsetSelected();
+ me._appDisplay.selectLastItem();
+ }
return true;
} else if (code == 116) {
- me._appDisplay.selectDown();
+ if (me._appDisplay.hasSelected() && !me._appDisplay.selectDown() && me._docDisplay.hasItems()) {
+ me._appDisplay.unsetSelected();
+ me._docDisplay.selectFirstItem();
+ } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectDown() && me._appDisplay.hasItems()) {
+ me._docDisplay.unsetSelected();
+ me._appDisplay.selectFirstItem();
+ }
return true;
}
return false;
font_name: "Sans Bold 14px",
text: "Applications",
x: SIDESHOW_PAD,
- y: this._searchEntry.y + this._searchEntry.height + 10,
+ y: this._searchEntry.y + this._searchEntry.height + SIDESHOW_SECTION_PAD,
height: 16});
this.actor.add_actor(appsText);
- let menuY = appsText.y + appsText.height + 6;
- this._appDisplay = new AppDisplay.AppDisplay(width, global.screen_height - menuY);
+ let sectionLabelHeight = appsText.height + SIDESHOW_SECTION_LABEL_PAD_BOTTOM
+ let menuY = appsText.y + sectionLabelHeight;
+
+ let itemDisplayHeight = global.screen_height - menuY - SIDESHOW_SECTION_PAD - sectionLabelHeight - SIDESHOW_PAD_BOTTOM;
+ this._appDisplay = new AppDisplay.AppDisplay(width, itemDisplayHeight / 2);
this._appDisplay.actor.x = SIDESHOW_PAD;
this._appDisplay.actor.y = menuY;
this.actor.add_actor(this._appDisplay.actor);
- /* Proxy the activated signal */
+ let docsText = new Clutter.Label({ color: SIDESHOW_TEXT_COLOR,
+ font_name: "Sans Bold 14px",
+ text: "Recent Documents",
+ x: SIDESHOW_PAD,
+ y: menuY + (itemDisplayHeight / 2) + SIDESHOW_SECTION_PAD,
+ height: 16});
+ this.actor.add_actor(docsText);
+
+ this._docDisplay = new DocDisplay.DocDisplay(width, itemDisplayHeight / 2);
+ this._docDisplay.actor.x = SIDESHOW_PAD;
+ this._docDisplay.actor.y = docsText.y + docsText.height + 6;
+ this.actor.add_actor(this._docDisplay.actor);
+
+ /* Proxy the activated signals */
this._appDisplay.connect('activated', function(appDisplay) {
- me.emit('activated');
+ me._searchEntry.text = '';
+ // we allow clicking on an item to launch it, and this unsets the selection
+ // so that we can move it to the item that was clicked on
+ me._appDisplay.unsetSelected();
+ me._docDisplay.unsetSelected();
+ me._appDisplay.doActivate();
+ me.emit('activated');
+ });
+ this._docDisplay.connect('activated', function(docDisplay) {
+ me._searchEntry.text = '';
+ // we allow clicking on an item to launch it, and this unsets the selection
+ // so that we can move it to the item that was clicked on
+ me._appDisplay.unsetSelected();
+ me._docDisplay.unsetSelected();
+ me._docDisplay.doActivate();
+ me.emit('activated');
+ });
+ this._appDisplay.connect('redisplayed', function(appDisplay) {
+ // This can be applicable if app display previously had the selection,
+ // but it got updated and now has no items, so we can try to move
+ // the selection to the doc display.
+ if (!me._appDisplay.hasSelected() && !me._docDisplay.hasSelected())
+ me._docDisplay.selectFirstItem();
+ });
+ this._docDisplay.connect('redisplayed', function(docDisplay) {
+ if (!me._docDisplay.hasSelected() && !me._appDisplay.hasSelected())
+ me._appDisplay.selectFirstItem();
});
},
show: function() {
- this._appDisplay.show();
+ this._appDisplay.selectFirstItem();
+ if (!this._appDisplay.hasSelected())
+ this._docDisplay.selectFirstItem();
+ else
+ this._docDisplay.unsetSelected();
+ this._appDisplay.show();
+ this._docDisplay.show();
+ },
+
+ hide: function() {
}
};
Signals.addSignalMethods(Sideshow.prototype);